﻿namespace Webinator
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Linq;
    using System.Text.RegularExpressions;
    using ExtensionMethods;
    using Impls;
    using Impls.WatiNImpl;
    using Impls.Webdriver;
    using OpenQA.Selenium;
    using OpenQA.Selenium.Chrome;
    using OpenQA.Selenium.Firefox;
    using OpenQA.Selenium.IE;
    using OpenQA.Selenium.Remote;
    using Util;
    using WatiN.Core;
    using Webinator.Impls.RemoteWebDriver;

    /// <summary>
    /// Creates <c>IWebManager</c> instances.
    /// </summary>
    public static class WebManagerFactory
    {
        private static readonly object _locker = new object();

        private static readonly object _lockerB = new object();
     
        private static List<IWebManager> _webManagers = new List<IWebManager>();

        /// <summary>
        /// Closes all web manager instances.
        /// </summary>
        public static void CloseInstances()
        {
            lock (_webManagers)
            {
                foreach (var webManager in _webManagers)
                {
                    webManager.CloseBrowser();
                }

                _webManagers.RemoveRange(0, _webManagers.Count);
            }
        }

        /// <summary>
        /// Creates <c>IWebManager</c> instances using the specified configuration.
        /// </summary>
        /// <param name="config">
        /// The configuration.
        /// </param>
        /// <param name="assertReady">
        /// When set to true, attempts to verify that the browser session is ready before returning.
        /// Useful when you are instantiating a large number of concurrent instances and are observing hung or dead browser sessions.
        /// </param>
        /// <returns>
        /// An <c>IWebManager</c> instance.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Unable to use framework.
        /// </exception>
        public static IWebManager CreateInstance(Config config, bool assertReady = false)
        {
            if (config.BaseUrl.StartsWith("www."))
            {
                config.BaseUrl = "http://" + config.BaseUrl;
            }
            else if (!config.BaseUrl.StartsWith("http://"))
            {
                config.BaseUrl = "http://www." + config.BaseUrl;
            }

            IWebManager webManager;
            switch (config.Framework)
            {
                case Config.AvailableFrameworks.WebDriver:
                    webManager = CreateWebDriverInstance(config);
                    break;
                case Config.AvailableFrameworks.WebDriverRemote:
                    webManager = CreateRemoteWebDriverInstance(config);
                    break;
                case Config.AvailableFrameworks.WatiN:
                    webManager = CreateWatiNInstance(config);
                    break;
                default:
                    throw new ArgumentOutOfRangeException(string.Format("Unable to use framework: {0}", config.Framework));
            }

            if (assertReady)
            {
                lock (_lockerB)
                {
                    // Wait for the page to become ready
                    var js = @"{ var temp=document.createElement('div'); temp.id='WebinatorJsListenerPosted';"
                                + @"b=document.getElementsByTagName('body')[0]; b.appendChild(temp); }";
                    webManager.RunJavaScript(js);
                    webManager.Wait(WaitUntil.Exists(LocateBy.Id("WebinatorJsListenerPosted"), 10000, false));
                    js = @"{var temp=document.getElementById('WebinatorJsListenerPosted'); temp.parentNode.removeChild(temp);}";
                    webManager.RunJavaScript(js);
                    webManager.DisplayMessageInBrowser("Browser ready.", 1000);
                } 
            }

            // Add web manager to list
            _webManagers.Add(webManager);

            return webManager;
        }

        [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
        internal static IWebManager CreateRemoteWebDriverInstance(Config config)
        {
            DesiredCapabilities capabilities;

            switch (config.Browser)
            {
                case Config.AvailableBrowsers.Chrome:
                    capabilities = DesiredCapabilities.Chrome();
                    break;
                case Config.AvailableBrowsers.Firefox:
                    capabilities = DesiredCapabilities.Firefox();
                    break;
                case Config.AvailableBrowsers.InternetExplorer:
                    capabilities = DesiredCapabilities.InternetExplorer();
                    break;
                case Config.AvailableBrowsers.Opera:
                    capabilities = DesiredCapabilities.Opera();
                    break;
                case Config.AvailableBrowsers.Android:
                    capabilities = DesiredCapabilities.Android();
                    break;
                case Config.AvailableBrowsers.IPad:
                    capabilities = DesiredCapabilities.IPad();
                    break;
                case Config.AvailableBrowsers.IPhone:
                    capabilities = DesiredCapabilities.IPhone();
                    break;
                case Config.AvailableBrowsers.HtmlUnit:
                    capabilities = DesiredCapabilities.HtmlUnit();
                    break;
                case Config.AvailableBrowsers.HtmlUnitWithJavaScript:
                    capabilities = DesiredCapabilities.HtmlUnitWithJavaScript();
                    break;
                case Config.AvailableBrowsers.FirefoxPlugins:
                case Config.AvailableBrowsers.FirefoxPerformance:
                case Config.AvailableBrowsers.FirefoxSsl:
                    throw new NotImplementedException("Not supported by this framework.");
                default:
                    throw new ArgumentOutOfRangeException();
            }

            foreach (var item in config.DesiredCapabilities)
            {
                capabilities.SetCapability(item.Key, item.Value);
            }

            IWebDriver driver = new ScreenshotRemoteWebDriver(new Uri(config.SeleniumGridHubUri), capabilities, config.CommandTimeout);
            driver.Navigate().GoToUrl(config.BaseUrl);

            var core = new WebDriverCore(driver, config);
            var highlighter = new Impls.WebDriver.WebDriverHighlighter(core);
            var log = new Impls.WebDriver.WebDriverLogUtil(core);
            var osActions = new RemoteOperatingSystemDomain();
            var web = new WebManager(core, highlighter, log, osActions);

            return web;
        }

        [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
        internal static IWebManager CreateWatiNInstance(Config config)
        {
            Browser browser;
            switch (config.Browser)
            {
                case Config.AvailableBrowsers.InternetExplorer:
                    browser = new IE(config.BaseUrl);
                    break;
                case Config.AvailableBrowsers.Firefox:
                    browser = new FireFox(config.BaseUrl);
                    //// TODO : This requires a jssh plugin for 3.x versions and lower.
                    break;
                case Config.AvailableBrowsers.Chrome:
                case Config.AvailableBrowsers.FirefoxPerformance:
                case Config.AvailableBrowsers.FirefoxPlugins:
                case Config.AvailableBrowsers.FirefoxSsl:
                case Config.AvailableBrowsers.HtmlUnitWithJavaScript:
                case Config.AvailableBrowsers.HtmlUnit:
                case Config.AvailableBrowsers.IPhone:
                case Config.AvailableBrowsers.IPad:
                case Config.AvailableBrowsers.Opera:
                case Config.AvailableBrowsers.Android:
                    throw new NotImplementedException("Not supported by this framework.");
                default:
                    throw new ArgumentOutOfRangeException();
            }

            var core = new WatiNCore(browser, config);
            var highlighter = new WatiNHighlighter(core);
            var log = new WatiNLogUtil(core);
            var osActions = new OperatingSystemDomain(core);
            var web = new WebManager(core, highlighter, log, osActions);

            return web;
        }

        [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
        internal static IWebManager CreateWebDriverInstance(Config config)
        {
            lock (_locker)
            {
                FirefoxProfile profile;
                FileUtil fileUtil;
                IWebDriver driver;

                switch (config.Browser)
                {
                    case Config.AvailableBrowsers.Chrome:
                        if (!string.IsNullOrEmpty(config.BrowserLocation))
                        {
                            var options = new ChromeOptions();
                            options.BinaryLocation = config.BrowserLocation;
                            driver = new ChromeDriver(options) { Url = config.BaseUrl };
                        }
                        else
                        {
                            driver = new ChromeDriver { Url = config.BaseUrl };
                        }

                        break;
                    case Config.AvailableBrowsers.FirefoxSsl:
                        profile = !string.IsNullOrEmpty(config.ProfileDirectory)
                                      ? new FirefoxProfile(config.ProfileDirectory)
                                      : new FirefoxProfile();

                        profile.AcceptUntrustedCertificates = true;

                        if (!string.IsNullOrEmpty(config.BrowserLocation))
                        {
                            var firefoxBinary = new FirefoxBinary(config.BrowserLocation);
                            driver = new FirefoxDriver(firefoxBinary, profile) { Url = config.BaseUrl };
                        }
                        else
                        {
                            driver = new FirefoxDriver(profile) { Url = config.BaseUrl };    
                        }
                        
                        break;
                    case Config.AvailableBrowsers.Firefox:
                        profile = !string.IsNullOrEmpty(config.ProfileDirectory)
                                      ? new FirefoxProfile(config.ProfileDirectory)
                                      : new FirefoxProfile();

                        if (!string.IsNullOrEmpty(config.BrowserLocation))
                        {
                            var firefoxBinary = new FirefoxBinary(config.BrowserLocation);
                            driver = new FirefoxDriver(firefoxBinary, profile) { Url = config.BaseUrl };
                        }
                        else
                        {
                            driver = new FirefoxDriver(profile) { Url = config.BaseUrl };
                        }

                        break;
                    case Config.AvailableBrowsers.FirefoxPerformance:
                        profile = !string.IsNullOrEmpty(config.ProfileDirectory)
                                      ? new FirefoxProfile(config.ProfileDirectory)
                                      : new FirefoxProfile();

                        fileUtil = new FileUtil();
                        var firebugPath = fileUtil.GetPathOfFileLike("firebug");
                        var netExportPath = fileUtil.GetPathOfFileLike("netexport");
                        Directory.CreateDirectory(config.NetExportDirectory);
                        profile.AddExtension(firebugPath);
                        profile.AddExtension(netExportPath);
                        profile.SetPreference("extensions.firebug.currentVersion", Regex.Replace(firebugPath, ".*-([0-9.]*).xpi$", "$1"));
                        profile.SetPreference("extensions.firebug.previousPlacement", 1);
                        profile.SetPreference("extensions.firebug.allPagesActivation", "on");
                        profile.SetPreference("extensions.firebug.onByDefault", true);
                        profile.SetPreference("extensions.firebug.activateSameOrigin", true);
                        profile.SetPreference("extensions.firebug.displayedByDefault", true);
                        profile.SetPreference("extensions.firebug.defaultPanelName", "net");
                        profile.SetPreference("extensions.firebug.net.enableSites", true);
                        profile.SetPreference("extensions.firebug.netexport.defaultLogDir", config.NetExportDirectory);
                        profile.SetPreference("extensions.firebug.netexport.alwaysEnableAutoExport", true);
                        profile.SetPreference("extensions.firebug.netexport.exportFromBFCache", true);

                        if (!string.IsNullOrEmpty(config.BrowserLocation))
                        {
                            var firefoxBinary = new FirefoxBinary(config.BrowserLocation);
                            driver = new FirefoxDriver(firefoxBinary, profile) { Url = config.BaseUrl };
                        }
                        else
                        {
                            driver = new FirefoxDriver(profile) { Url = config.BaseUrl };
                        }

                        break;
                    case Config.AvailableBrowsers.FirefoxPlugins:
                        profile = !string.IsNullOrEmpty(config.ProfileDirectory)
                                      ? new FirefoxProfile(config.ProfileDirectory)
                                      : new FirefoxProfile("temp".AbsolutePath());

                        profile.AcceptUntrustedCertificates = true;
                        fileUtil = new FileUtil();
                        var plugins = fileUtil.GetPathOfFilesLike(".xpi").ToList();
                        foreach (var plugin in plugins)
                        {
                            profile.AddExtension(plugin);
                        }

                        // Configure known plugins: firefinder/firepath/yslow need no configuration
                        if (plugins.Any(p => p.ToLower().Contains("disable_add_on_compatibility_checks")))
                        {
                            profile.SetPreference("extensions.checkCompatibility", false);
                        }

                        if (plugins.Any(p => p.ToLower().Contains("firebug")))
                        {
                            var firebugVersion = Regex.Replace(plugins.First(p => p.Contains("firebug")), ".*-([0-9.]+).*.xpi$", "$1");
                            profile.SetPreference("extensions.firebug.currentVersion", firebugVersion);
                            profile.SetPreference("extensions.firebug.previousPlacement", 1);
                            profile.SetPreference("extensions.firebug.allPagesActivation", "on");
                            profile.SetPreference("extensions.firebug.onByDefault", true);
                            profile.SetPreference("extensions.firebug.activateSameOrigin", true);
                            profile.SetPreference("extensions.firebug.displayedByDefault", true);
                            profile.SetPreference("extensions.firebug.defaultPanelName", "net");
                            profile.SetPreference("extensions.firebug.net.enableSites", true);
                            profile.SetPreference("extensions.firebug.script.enableSites", true);
                            profile.SetPreference("extensions.firebug.console.enableSites", true);
                        }

                        if (plugins.Any(p => p.ToLower().Contains("flashbug")))
                        {
                            profile.SetPreference("extensions.firebug.flbConsole.enableSites", true);
                            profile.SetPreference("extensions.firebug.flbDecompiler.enableSites", true);
                            profile.SetPreference("extensions.firebug.flbInspector.enableSites", true);
                            profile.SetPreference("extensions.firebug.flbSharedObjects.enableSites", true);
                        }

                        if (plugins.Any(p => p.ToLower().Contains("firecookie")))
                        {
                            profile.SetPreference("extensions.firebug.cookies.enableSites", true);
                        }

                        if (plugins.Any(p => p.ToLower().Contains("netexport")))
                        {
                            profile.SetPreference("extensions.firebug.netexport.defaultLogDir", config.NetExportDirectory);
                            profile.SetPreference("extensions.firebug.netexport.exportFromBFCache", true);
                        }

                        if (!string.IsNullOrEmpty(config.BrowserLocation))
                        {
                            var firefoxBinary = new FirefoxBinary(config.BrowserLocation);
                            driver = new FirefoxDriver(firefoxBinary, profile) { Url = config.BaseUrl };
                        }
                        else
                        {
                            driver = new FirefoxDriver(profile) { Url = config.BaseUrl };
                        }

                        break;
                    case Config.AvailableBrowsers.InternetExplorer:
                        driver = new InternetExplorerDriver { Url = config.BaseUrl };
                        break;
                    case Config.AvailableBrowsers.HtmlUnitWithJavaScript:
                    case Config.AvailableBrowsers.HtmlUnit:
                    case Config.AvailableBrowsers.IPhone:
                    case Config.AvailableBrowsers.IPad:
                    case Config.AvailableBrowsers.Opera:
                    case Config.AvailableBrowsers.Android:
                        throw new NotImplementedException("Not supported by this framework.");
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                var core = new WebDriverCore(driver, config);
                var highlighter = new Impls.WebDriver.WebDriverHighlighter(core);
                var log = new Impls.WebDriver.WebDriverLogUtil(core);
                var osActions = new OperatingSystemDomain(core);
                var web = new WebManager(core, highlighter, log, osActions);

                return web;
            }
        }
    }
}
